/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is Save File to.
 *
 * The Initial Developer of the Original Code is
 * Atte Kemppilä.
 * Portions created by the Initial Developer are Copyright (C) 2009
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */


var savefileto =
{
    types: ["image", "link", "page"],
    
    showType:
    {
        get image() { return (gContextMenu.onLoadedImage || gContextMenu.onCanvas); },
        get link() { return gContextMenu.onSaveableLink; },
        get page() { return (! (gContextMenu.onTextInput || gContextMenu.onLink || gContextMenu.isContentSelected || gContextMenu.onImage || gContextMenu.onCanvas || gContextMenu.onVideo || gContextMenu.onAudio)); }
    },
    
    onLoad: function()
    {
        Cu.import("resource://savefileto/constants.jsm", this);
        
        var self = this;
        AddonManager.getAddonByID(this.EXT_ID, function(addon) { self.onLoadWithAddon(addon); });
    },
    
    onLoadWithAddon: function(addon)
    {
        // load os specific default preferences
        Cu.import("resource://savefileto/default-preferences.jsm", this);
        var prefsPath = "platform/" + Services.appinfo.OS + "/defaults/preferences/savefileto.js";
        this.loadDefaultPrefs(addon, prefsPath);
        
        Cu.import("resource://savefileto/private-browsing.jsm");
        
        var self = this;
        
        // _init() isn't called until the first time context menu is opened
        // onContextMenuShowing() is wrapped, so that it can be removed later
        this._onContextMenuShowing = function(e) { self.onContextMenuShowing(e); };
        var contextMenu = document.getElementById("contentAreaContextMenu");
        contextMenu.addEventListener("popupshowing", this._onContextMenuShowing, false);
    },
    
    onUnload: function()
    {
        if (this.accessKeyChangeListener)
            this.savefileto.prefs.removeListener(this.accessKeyChangeListener);
    },
    
    onContextMenuShowing: function(e)
    {
        // don't call _init() when MenuEditor fake opens context menu at startup
        if (! gContextMenu)
            return;
        
        var contextMenu = document.getElementById("contentAreaContextMenu");
        contextMenu.removeEventListener("popupshowing", this._onContextMenuShowing, false);
        delete this._onContextMenuShowing;
        
        this._init(e);
    },
    
    _init: function(e)
    {
        Cu.import("resource://savefileto/savefileto.jsm", this);
        
        var self = this;
        var prefs = this.savefileto.prefs;
        
        for each (var _type in this.types)
        {
            let type = _type;
            
            let menuId = "savefileto-context-save" + type;
            let menu = document.getElementById(menuId);
            
            // show/hide menu item
            let hidden = function() (! (prefs[type + "Enabled"] && self.showType[type]));
            let hide = this._addMenuItemHide(menu, hidden);
            hide(e);
            
            // init access key
            this._updateMenuItemAccessKey(type);
            
            // init cascade menu
            let commandCallback = function(event, folder)
            {
                var prompt = (event.ctrlKey || prefs.prompt);
                window["savefileto_save_" + type].call(self, folder, prompt);
                self.savefileto.addToRecent(type, folder);
            }
            
            let cascadeMenu = this.savefileto.createCascadeMenu(type, commandCallback);
            cascadeMenu.attach(menu);
        }
        
        // observe access key changes
        this.accessKeyChangeListener = function(prefs, propName)
        {
            var match = /^(page|link|image)AccessKey$/.exec(propName);
            if (match)
                self._updateMenuItemAccessKey(match[1]);
        };
        prefs.addListener(this.accessKeyChangeListener);
    },
    
    _addMenuItemHide: function(menuItem, hidden)
    {
        var menuPopup = menuItem.parentNode;
        var hide = function() { menuItem.hidden = hidden(); }
        menuPopup.addEventListener("popupshowing", hide, false);
        return hide;
    },
    
    _updateMenuItemAccessKey: function(type)
    {
        var menuId = "savefileto-context-save" + type;
        var menu = document.getElementById(menuId);
        menu.accessKey = this.savefileto.prefs[type + "AccessKey"];
    }
}


/*
 * http://kb.mozillazine.org/Firefox_:_FAQs_:_About:config_Entries#Browser..2A
 * 
 * browser.download
 *  .useDownloadDir        bool  false: prompt; true: no prompt, download to folder defined in .folderList
 *  .folderList            int   0: desktop; 1: downloads folder; 2: folder in .dir
 *  .dir                   str   user defined download folder
 *  .lastDir               str   last folder used in Save Xxx As...
 *  .save_converter_index  int   the format to save web pages in (Save Page As...)
 *
 * .lastDir does not affect downloading but it is overwritten after it.
 * The original value is preserved because we don't want to affect the normal
 * behavior of Save Xxx As....
 * .save_converter_index on the other hand is always prompted when Save Page to
 * is used (skipPrompt has no effect) so there is no point in keeping the original value.
 *
 * See toolkit.jar:content/global/contentAreaUtils.js:getTargetFile() for actual implementation.
 *
 * Note that ff3's saveLink() doesn't honor the above preferences.
 * See savefileto_saveLink_prompt() below for details.
 */


// based on browser.jar:content/browser/browser.js:nsContextMenu.saveImage()  (context menu > Save Image as...)
function savefileto_save_image(folder, prompt)
{
    // ff3.1+ uses mediaURL
    var imageUrl = (gContextMenu.mediaURL ? gContextMenu.mediaURL : gContextMenu.imageURL);
    // ff3 has no docURL
    var doc = gContextMenu.target.ownerDocument;
    var docUri = (gContextMenu.docURL ? makeURI(gContextMenu.docURL) : doc.documentURIObject);
    var principal = (gContextMenu.docURL ? gContextMenu.docURL : doc.nodePrincipal);
    urlSecurityCheck(imageUrl, principal);
    var prefsBackup = this.savefileto.getDownloadPrefs(docUri);
    try
    {
        var prefs = {useDownloadDir: (! prompt), folderList: 2, dir: folder.file, lastDir: folder.file};
        var lastDownloadDir = {uri: docUri, file: folder.file};
        this.savefileto.setDownloadPrefs({downloadPrefs: prefs, lastDownloadDir: lastDownloadDir});
        // toolkit.jar:content/global/contentAreaUtils.js
        // saveImageURL(aURL, aFileName, aFilePickerTitleKey, aShouldBypassCache, aSkipPrompt, aReferrer)
        saveImageURL(imageUrl, null, "SaveImageTitle", false, (! prompt), docUri);
    }
    finally
    {
        // restore original browser download preferences
        this.savefileto.setDownloadPrefs(prefsBackup);
    }
}


/* 
 * It turns out that there's no "built-in" Content-Disposition filename support
 * in ff2. This is fixed in bug 299372 for ff3, but it only handles situations 
 * where filename is prompted, so no-prompt download must be made by hand. 
 */

function savefileto_save_link(folder, prompt)
{
    if (prompt)
        savefileto_save_link_prompt.call(this, folder);
    else
        savefileto_save_link_noPrompt.call(this, folder);
}


function savefileto_save_link_prompt(folder)
{
    var doc = gContextMenu.target.ownerDocument;
    var docUri = doc.documentURIObject;
    var prefsBackup = this.savefileto.getDownloadPrefs(docUri);
    try
    {
        var prompt = true;
        var prefs = {useDownloadDir: (! prompt), folderList: 2, dir: folder.file, lastDir: folder.file};
        var lastDownloadDir = {uri: docUri, file: folder.file};
        this.savefileto.setDownloadPrefs({downloadPrefs: prefs, lastDownloadDir: lastDownloadDir});
        // browser.jar:content/browser/browser.js:nsContextMenu.saveLink()  (context menu > Save Link as...)
        gContextMenu.saveLink();
    }
    finally
    {
        // ff3 & saveLink():
        //  - doesn't honor |browser.download.*| prefs, it uses always |lastDir|
        //  - download dir is determined after saveLink() has returned, so our change-saveLink()-restore workaround doesn't work
        //
        // So the only option is NOT to restore |lastDir| to its original value.
        prefsBackup.downloadPrefs.lastDir = prefs.lastDir;
        
        // same thing with per-site download dir - don't restore original value
        prefsBackup.lastDownloadDir = null;
        
        // restore original browser download preferences
        this.savefileto.setDownloadPrefs(prefsBackup);
    }
}


function savefileto_save_link_noPrompt(folder)
{
    var linkUrl = gContextMenu.linkURL;
    // ff3 has no docURL
    var doc = gContextMenu.target.ownerDocument;
    var docUri = (gContextMenu.docURL ? makeURI(gContextMenu.docURL, doc.characterSet) : doc.documentURIObject);
    var principal = (gContextMenu.docURL ? gContextMenu.docURL : doc.nodePrincipal);
    urlSecurityCheck(linkUrl, principal);
    
    // source url
    var sourceUri = gContextMenu.getLinkURI();
    sourceUri.QueryInterface(Ci.nsIURL);
    
    // target dir, file, uri
    var targetDir = folder.file;
    var targetFileName = getDefaultFileName(null, sourceUri, docUri, null);
    var targetFile = savefileto_getTargetFile(targetDir, targetFileName);
    var targetUri = Services.io.newFileURI(targetFile);
    
    // nsIWebProgressListener to listen possible content-disposition
    function WebProgressListener()
    {
        this.creationTime = Date.now();
    };
    
    WebProgressListener.prototype =
    {
        // nsISupports
        QueryInterface: function(iid)
        {
            if (iid.equals(Ci.nsIWebProgressListener) ||
                iid.equals(Ci.nsIWebProgressListener2) ||
                iid.equals(Ci.nsISupports))
                return this;
            throw Cr.NS_ERROR_NO_INTERFACE;
        },
        
        // nsIWebProgressListener2
        onProgressChange64: function(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {},
        onRefreshAttempted: function(webProgress, refreshURI, millis, sameURI) {},
        
        // nsIWebProgressListener
        onLocationChange: function(webProgress, request, location) {},
        onProgressChange: function(webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {},
        onSecurityChange: function(webProgress, request, state) {},
        onStatusChange: function(webProgress, request, status, message) {},
        
        onStateChange: function(webProgress, request, stateFlags, status)
        {
            this._onStateChange(webProgress, request, stateFlags, status);
            if (this.transfer)
                this.transfer.onStateChange(webProgress, request, stateFlags, status);
        },
        
        _onStateChange: function(webProgress, request, stateFlags, status)
        {
            if (stateFlags & Ci.nsIWebProgressListener.STATE_START)
            {
                if (this.transfer)
                    return;
                
                var trueTargetUri = targetUri;
                
                try
                {
                    // change target file
                    // if request has been redirected, channel.URI != sourceUri (== channel.originalURI)
                    // or if there's content disposition header that which contains filename
                    var channel = request.QueryInterface(Ci.nsIHttpChannel);
                    var contentDisposition = null;
                    try { contentDisposition = channel.getResponseHeader("Content-Disposition"); } catch (e) { /* ignore */ }
                    var trueFilename = getDefaultFileName(null, channel.URI, docUri, contentDisposition);
                    if (trueFilename != targetFileName)
                    {
                        var trueTargetFile = savefileto_getTargetFile(targetDir, trueFilename);
                        trueTargetUri = Services.io.newFileURI(trueTargetFile);
                        this.trueTargetFilename = trueTargetFile.leafName;
                        this.tempFile = targetFile;
                    }
                }
                catch (e if (e.result == Cr.NS_ERROR_NO_INTERFACE)) { /* ignore */ }
                
                this.initTransfer(trueTargetUri, this.tempFile);
            }
            else if (stateFlags & Ci.nsIWebProgressListener.STATE_STOP)
            {
                if (! this.tempFile)
                    return;
                
                // rename temp file to true target file
                this.tempFile.moveTo(null, this.trueTargetFilename);
                delete this.tempFile;
            }
        },
        
        initTransfer: function(trueTargetUri, tempFile)
        {
            if (this.transfer)
                return;
            
            // ff1.5 seems to leak if notify isn't removed
            timer.cancel();
            delete callback.notify;
            
            var transfer = this.transfer = Cc["@mozilla.org/transfer;1"].createInstance(Ci.nsITransfer);
            var displayName = null;
            var mimeInfo = null;
            var time = this.creationTime * 1000;
            transfer.init(sourceUri, trueTargetUri, displayName, mimeInfo, time, tempFile, persist);
            
            // nsIWebProgressListener2.onRefreshAttempted is ff3 only
            for each (var _name in ["onLocationChange", "onProgressChange", "onSecurityChange", "onStatusChange", "onProgressChange64", "onRefreshAttempted"])
            {
                let name = _name;
                if (transfer[name])
                    this[name] = function() { transfer[name].apply(transfer, arguments); };
            }
        }
    }
    
    // nsIWebBrowserPersist
    var persist = Cc["@mozilla.org/embedding/browser/nsWebBrowserPersist;1"].createInstance(Ci.nsIWebBrowserPersist);
    var progressListener = new WebProgressListener(persist);
    persist.progressListener = progressListener;
    persist.persistFlags =
        Ci.nsIWebBrowserPersist.PERSIST_FLAGS_AUTODETECT_APPLY_CONVERSION |
        Ci.nsIWebBrowserPersist.PERSIST_FLAGS_BYPASS_CACHE |
        Ci.nsIWebBrowserPersist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
    
    // timeout for waiting the HTTP headers
    // otherwise it would be impossible to cancel the download from the ui
    var timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var callback = {notify: function() { progressListener.initTransfer(targetUri, null); }};
    var timeout = this.savefileto.prefs.responseTimeout;
    timer.initWithCallback(callback, timeout, Ci.nsITimer.TYPE_ONE_SHOT);
    
    var referer = doc.documentURIObject;
    persist.saveURI(sourceUri, null, referer, null, null, targetUri);
}


function savefileto_save_page(folder, prompt)
{
    var doc = gContextMenu.target.ownerDocument;
    var docUri = doc.documentURIObject;
    var prefsBackup = this.savefileto.getDownloadPrefs(docUri);
    try
    {
        var prefs = {useDownloadDir: (! prompt), folderList: 2, dir: folder.file, lastDir: folder.file};
        var lastDownloadDir = {uri: docUri, file: folder.file};
        this.savefileto.setDownloadPrefs({downloadPrefs: prefs, lastDownloadDir: lastDownloadDir});
        // toolkit.jar:content/global/contentAreaUtils.js
        // saveDocument(aDocument, aSkipPrompt)
        // aSkipPrompt has no effect - prompts always
        saveDocument(window.content.document, (! prompt));
    }
    finally
    {
        // restore original browser download preferences
        this.savefileto.setDownloadPrefs(prefsBackup);
    }
}


function savefileto_getTargetFile(dir, filename)
{
    var file = dir.clone();
    file.append(filename);
    
    // copy paste from toolkit.jar:content/global/contentAreaUtils.js:getTargetFile()
    var collisionCount = 0;
    while (file.exists()) {
        collisionCount++;
        if (collisionCount == 1) {
            // Append "(2)" before the last dot in (or at the end of) the filename
            // special case .ext.gz etc files so we don't wind up with .tar(2).gz
            if (file.leafName.match(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i))
                file.leafName = file.leafName.replace(/\.[^\.]{1,3}\.(gz|bz2|Z)$/i, "(2)$&");
            else
                file.leafName = file.leafName.replace(/(\.[^\.]*)?$/, "(2)$&");
        }
        else {
            // replace the last (n) in the filename with (n+1)
            file.leafName = file.leafName.replace(/^(.*\()\d+\)/, "$1" + (collisionCount+1) + ")");
        }
    }
    
    return file;
}


window.addEventListener("load", function() { savefileto.onLoad(); }, false);
window.addEventListener("unload", function() { savefileto.onUnload(); }, false);
